<!doctype html>
<!--
Beatboxer
Inspired by https://www.youtube.com/watch?v=6O_92BTrUcA
-->
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="x-ua-compatible" content="ie=edge" />
  <meta name="description" itemprop="description" content="Beatboxer is a drum machine in about 100 lines of html/js/css" />
  <meta itemprop="name" content="Beatboxer" />
  <meta itemprop="image" content="http://sig.gy/beatboxer/thumbnail.png" />

  <!-- Facebook Open Graph -->
  <meta property="og:title" content="Beatboxer" />
  <meta property="og:description" content="Beatboxer is a drum machine in about 100 lines of html/js/css" />
  <meta property="og:url" content="http://sig.gy/beatboxer/" />
  <meta property="og:type" content="website" />
  <meta property="og:image" content="http://sig.gy/beatboxer/thumbnail.png" />
  <meta property="og:site_name" content="Beatboxer" />

  <!-- Twitter Card -->
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:site" content="Beatboxer" />
  <meta name="twitter:title" content="Beatboxer" />
  <meta name="twitter:description" content="Beatboxer is a drum machine in about 100 lines of html/js/css" />
  <meta name="twitter:image:src" content="http://sig.gy/beatboxer/thumbnail.png" />
  <meta name="twitter:url" content="http://sig.gy/beatboxer/" />

  <title>Beatboxer</title>

  <link rel="canonical" href="http://sig.gy/beatboxer/" />

  <style>
    body {
      background-color: black;
    }

    p {
      margin: 0;
    }

    button.beat {
      background-color: black;
      padding: 10px;
      border: 10px solid white;
      margin: 5px;
      cursor: pointer;
      width: 0;
      height: 0;
    }

    button.on {
      background-color: red;
      border-color: red;
    }

    button.ticked {
      background-color: white;
      border-color: white;
    }

    #grid {
      width: 800px;
      height: 200px;
      position: absolute;
      top:0;
      bottom: 0;
      left: 0;
      right: 0;
      margin: auto;
    }

    .github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}
  </style>
</head>
<body>
  <div id="grid"></div>

  <a href="https://github.com/siggy/beatboxer" class="github-corner" aria-label="Fork me on Github" title="Fork me on Github">
    <svg width="80" height="80" viewBox="0 0 250 250" style="fill:#fff; color:#151513; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg>
  </a>

  <script>
    const BPM = 120;
    const TICKS = 16;
    const INTERVAL = 1 / (4 * BPM / (60 * 1000));
    const MAX_BITS = 32;
    const MAX_HEX = MAX_BITS / 4;

    // sounds originated from http://808.html909.com
    const sounds = [
      'sounds/bass_drum.wav',
      'sounds/snare_drum.wav',
      'sounds/low_tom.wav',
      'sounds/mid_tom.wav',

      // 'sounds/hi_tom.wav',
      // 'sounds/rim_shot.wav',
      // 'sounds/hand_clap.wav',
      // 'sounds/cowbell.wav',
      // 'sounds/cymbal.wav',
      // 'sounds/o_hi_hat.wav',
      // 'sounds/cl_hi_hat.wav',

      // 'sounds/low_conga.wav',
      // 'sounds/mid_conga.wav',
      // 'sounds/hi_conga.wav',
      // 'sounds/claves.wav',
      // 'sounds/maracas.wav',
    ];

    const audioCtx = new(window.AudioContext || window.webkitAudioContext)();
    const buffers = [];
    const theGrid = document.getElementById('grid');
    const sLen = sounds.length;

    function binToHex(bin) {
      var hex = '';
      for (i = 0, len = bin.length; i < len; i += MAX_BITS) {
        var tmp = parseInt(bin.substr(i,MAX_BITS), 2).toString(16);
        while (tmp.length < MAX_HEX) {
          tmp = '0' + tmp;
        }
        hex += tmp;
      }
      return hex;
    }

    function hexToBin(hex) {
      var bin = '';
      for (i = 0, len = hex.length; i < len; i += MAX_HEX) {
        var tmp = parseInt(hex.substr(i,MAX_HEX), 16).toString(2);
        while (tmp.length < MAX_BITS) {
          tmp = '0' + tmp;
        }
        bin += tmp;
      }
      return bin;
    }

    // represent the hash as two 32-bit integers, in hex
    function updateState() {
      var state = '';

      Array.from(beats).map(function(btn) {
        state += btn.classList.contains('on') ? '1' : '0';
      });

      // TODO: this causes an unnecessary restore state
      window.location.hash = binToHex(state);
    }

    function restoreState() {
      var hash = window.location.hash;
      hash = (hash === '') ? '0000000000000000' : hash.substr(1); // Remove the first char (#...)

      hexToBin(hash).split('').map(function(el, i) {
        if (parseInt(el) === 1) {
          beats[i].classList.add('on');
        } else {
          beats[i].classList.remove('on');
        }
      });
    }

    // needed for page history
    window.addEventListener('hashchange', restoreState, false);

    for (var soundIndex = 0; soundIndex < sLen; ++soundIndex) {
      (function (index) {
        // create buffer for sound
        const req = new XMLHttpRequest();
        req.open('GET', sounds[index], true);
        req.responseType = 'arraybuffer';
        req.onload = function () {
          audioCtx.decodeAudioData(
            req.response,
            function (buffer) {
              buffers.push(buffer);
            }
          );
        };
        req.send();
      })(soundIndex);

      // create row for sound
      const fragment = document.createDocumentFragment();

      for (var t = 0; t < TICKS; ++t) {
        const btn = document.createElement('button');
        btn.className = 'beat';
        btn.addEventListener('click', function () {
          this.classList.toggle('on');
          updateState();
        }, false);

        fragment.appendChild(btn);
      }

      theGrid.appendChild(fragment);
      theGrid.appendChild(document.createElement('p'));
    }

    const beats = document.getElementsByClassName('beat');

    var lastTick = TICKS - 1;
    var curTick = 0;

    var lastTime = new Date().getTime();

    function drumLoop() {
      const curTime = new Date().getTime();

      if (curTime - lastTime >= INTERVAL) {
        for (var i = 0; i < sLen; ++i) {
          const lastBeat = beats[i * TICKS + lastTick];
          const curBeat = beats[i * TICKS + curTick];

          lastBeat.classList.remove('ticked');
          curBeat.classList.add('ticked');

          if (curBeat.classList.contains('on')) {
            try {
              const source = audioCtx.createBufferSource();
              source.buffer = buffers[i];
              source.connect(audioCtx.destination);
              source.start();
            } catch (e) {
              console.error(e.message);

              // Fallback method
              new Audio(sounds[i]).play();
            }
          }
        }

        lastTick = curTick;
        curTick = (curTick + 1) % TICKS;
        lastTime = curTime;
      }
      requestAnimationFrame(drumLoop);
    }
    // Restore the state before we start the drum loop
    restoreState();
    requestAnimationFrame(drumLoop);

    // adapted from https://paulbakaus.com/tutorials/html5/web-audio-on-ios/
    function enableIOSAudio() {
      const buffer = audioCtx.createBuffer(1, 1, 22050);
      const source = audioCtx.createBufferSource();

      source.buffer = buffer;
      source.connect(audioCtx.destination);
      source.noteOn(0);

      window.removeEventListener('touchend', enableIOSAudio, false);
    }
    window.addEventListener('touchend', enableIOSAudio, false);
  </script>
</body>
</html>
